// Copyright (c) Microsoft Corporation. All rights reserved.

namespace Microsoft.Tools.CodeRunner.Scaffold
{
	using System;
	using System.IO;
	using System.Security.Policy;
	using System.Reflection;
	using System.Diagnostics;
	using System.Text;
	using System.Text.RegularExpressions;
	using System.Collections;
	using System.Collections.Generic;
	using System.Collections.Specialized;
	using System.Runtime.InteropServices;
	using System.Xml;
	using System.Xml.XPath;
	using System.Configuration;
	using System.Threading;
	using System.ComponentModel;
	using System.Security;
	using Microsoft.Win32;
    using Microsoft.Tools.CodeRunner;
    using RootNamespace = Microsoft.Tools.CodeRunner;

	internal enum MessageType
	{
		Normal,
		Verbose,
		Warning,
		Error,
	};

	internal enum ScaffoldType
	{
		Unknown,
		CSharp,
		VisualBasic,
		JScript,
		JSharp
	};

	internal class VisualStudioInfo
	{
		public VisualStudioInfo(string devEnvExe)
		{
			this.devEnvExe = devEnvExe;
		}
		
		private FileVersionInfo version;
		private string devEnvExe;

		public FileVersionInfo Version
		{
			get
			{
				if (version == null)
				{
					version = FileVersionInfo.GetVersionInfo(devEnvExe);
				}
				
				return version;
			}
		}
		
		public string DevEnvExe
		{
			get
			{
				return devEnvExe;
			}
		}
	}

	internal class ProgramOptions
	{
		#region Constructor
		public ProgramOptions()
		{
			try
			{
				Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

				// Need to deal with .config file not existing 
				if (config.HasFile)
				{
					verbose = GetBoolSetting(config, "Verbose");
					scrapeReferences = GetBoolSetting(config, "ScrapeReferences");
					
					string tmp = GetStringSetting(config, "ProgramTemplate");
					
					if (tmp != null)
						programTemplate = new ParsedPath(tmp);
				}
			}
			catch (ConfigurationErrorsException)
			{
				Program.WriteMessage(MessageType.Warning, "Application configuration could not be loaded");
			}
		}

		#endregion

		#region Instance Properties

		[CommandLineArgument("help", Description = "HelpSwitchDescription", ShortName = "?", ValueHint = "")]
		public bool ShowHelp
		{
			get
			{
				return showHelp;
			}
			set
			{
				showHelp = value;
			}
		}

		[CommandLineArgument("verbose", Description = "VerboseSwitchDescription", ShortName = "v", ValueHint = "")]
		public bool Verbose
		{
			get
			{
				return verbose;
			}
			set
			{
				verbose = value;
			}
		}

		[CommandLineArgument("template", Description = "ProgramTemplateSwitchDescription", ShortName = "t", ValueHint = "ProgramTemplateSwitchHint")]
		public ParsedPath ProgramTemplate
		{
			get
			{
				return programTemplate;
			}
			set
			{
				programTemplate = value;
			}
		}

		[DefaultCommandLineArgument("script", ValueHint = "ScriptSwitchHint")]
		public ParsedPath ScriptPath
		{
			get 
			{ 
				return scriptPath; 
			}
			set 
			{ 
				scriptPath = value;
				scaffoldType = GetScaffoldTypeFromExtension(scriptPath.Extension);
			}
		}

		public bool ScrapeReferences
		{
			get { return scrapeReferences; }
		}

		public ScaffoldType ScaffoldType
		{
			get { return scaffoldType; }
		}
		
		#endregion

		#region Class Methods

		private static ScaffoldType GetScaffoldTypeFromExtension(string ext)
		{
			switch (ext)
			{
				case ".cs":
				case ".csr":
					return ScaffoldType.CSharp;

				default:
					return ScaffoldType.Unknown;
			}
		}

		private static bool GetBoolSetting(Configuration config, string setting)
		{
			bool val = false;

			try
			{
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting];
					
					if (element != null)
					{
						val = (CaseInsensitiveComparer.Default.Compare(element.Value, ScaffoldResources.No) != 0 &&
							CaseInsensitiveComparer.Default.Compare(element.Value, ScaffoldResources.False) != 0);
					}
				}
			}
			catch (ConfigurationException e)
			{
				Program.WriteMessage(MessageType.Warning, e.Message);
			}

			return val;
		}

		private static string GetStringSetting(Configuration config, string setting)
		{
			string val = null;

			try
			{
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting];
					
					if (element != null)
					{
						val = element.Value;
					}
				}
			}
			catch (ConfigurationException e)
			{
				Program.WriteMessage(MessageType.Warning, e.Message);
			}

			if (val != null && val.CompareTo(String.Empty) == 0)
				val = null;

			return val;
		}

		private int GetInt32Setting(Configuration config, string setting, int defVal)
		{
			int val = defVal;

			try
			{
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting];
					
					if (element != null)
					{
						string tmp = element.Value;
						
						if (tmp != null)
							val = Int32.Parse(tmp);
					}
				}
			}
			catch (ConfigurationException e)
			{
				Program.WriteMessage(MessageType.Warning, e.Message);
			}

			return val;
		}

		#endregion

		#region Instance Data

		private bool showHelp;
		private ParsedPath scriptPath;
		private ParsedPath programTemplate;
		private bool verbose;
		private bool scrapeReferences;
		private ScaffoldType scaffoldType;

		#endregion
	}

	class Program
	{
		internal static void WriteMessage(MessageType s, string format, params object[] args)
		{
			switch (s)
			{
				default:
				case MessageType.Normal:
					Console.Out.WriteLine(format, args);
					return;
				case MessageType.Verbose:
					Console.ForegroundColor = ConsoleColor.DarkGray;
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
				case MessageType.Warning:
					Console.ForegroundColor = ConsoleColor.Yellow;
					Console.Error.Write(ScaffoldResources.Warning);
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
				case MessageType.Error:
					Console.ForegroundColor = ConsoleColor.Red;
					Console.Error.Write(ScaffoldResources.Error);
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
			}
		}

		static int Main(string[] args)
		{
			ProgramOptions options = new ProgramOptions();

			if (!ProcessCommandLine(options, args))
				return 1;

			return new Scaffold(options).ConstructAndUseScaffold();
		}

		private static bool ProcessCommandLine(ProgramOptions options, string[] args)
		{
			CommandLineParser parser = new CommandLineParser(typeof(ProgramOptions), typeof(ScaffoldResources));

			try
			{
				parser.Parse(args);
				parser.SetTarget(options);

				// If the help option appears anywhere, just show the help
				if (options.ShowHelp)
				{
					Console.Write(parser.LogoBanner);
					Console.Write(parser.Usage);
					WriteLineWithWordBreaks(ScaffoldResources.WithNoSwitchesSpecified);
					return false;
				}

				// At this point, we need a program (the default argument)
				if (options.ScriptPath == null)
					throw new CommandLineArgumentException(ScaffoldResources.NoSourceSpecified);

				// Fully qualify the program
				options.ScriptPath = options.ScriptPath.MakeFullPath();

				if (options.ScriptPath.File == String.Empty)
					throw new CommandLineArgumentException(ScaffoldResources.NoEmptyFilename);
				else if (options.ScriptPath.HasWildcards)
					throw new CommandLineArgumentException(ScaffoldResources.NoSourceWildcards);
				else if (options.ScaffoldType == ScaffoldType.Unknown)
					throw new CommandLineArgumentException(ScaffoldResources.FileNameMustEndIn);

				// TODO: More project types coming soon
				if (options.ScaffoldType != ScaffoldType.CSharp)
					throw new CommandLineArgumentException("Non-C# projects are not yet supported.");
			}
			catch (CommandLineArgumentException exp)
			{
				Console.Error.WriteLine();
				WriteMessage(MessageType.Error, "{0}", exp.Message);

				return false;
			}
			catch (SystemException exp)
			{
				WriteMessage(MessageType.Error, "{0}\n", exp.Message);
				return false;
			}
			
			return true;
		}

		public static VisualStudioInfo GetVisualStudioInfo()
		{
			string exeName = null;
			string[] vsRegistryRoots = 
			{
				@"SOFTWARE\Microsoft\VisualStudio\9.0",
				@"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0",
				@"SOFTWARE\Microsoft\VisualStudio\8.0",
				@"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\8.0",
			};
			
			RegistryKey rk = null;
			
			for (int i = 0; i < vsRegistryRoots.Length; ++i)
			{
				rk = Registry.LocalMachine.OpenSubKey(vsRegistryRoots[i]);
				
				if (rk != null)
				{
					// We have a key, but do we have the Installdir value?  If we are running on x64, then 
					// the actual 32-bit VS registry keys will be under Wow6432Node.
					try
					{
						object obj = rk.GetValue("InstallDir");

						if (obj != null)
						{
							exeName = obj.ToString();
							exeName += (exeName.EndsWith(@"\") ? "" : @"\") + "devenv.exe";
							
							break;
						}
					}
					catch (Exception e)
					{
						if (!(e is SecurityException ||
							e is UnauthorizedAccessException ||
							e is IOException))
						{
							throw e;
						}
					}
				}
			}

			if (exeName == null)
			{
				WriteMessage(MessageType.Error, ScaffoldResources.VSNotInstalled);
				return null;
			}
			
			return new VisualStudioInfo(exeName);
		}

		public static void WriteLineWithWordBreaks(string text)
		{
			int lineLength = Console.BufferWidth < 2 ? Console.WindowWidth - 1 : Console.BufferWidth - 1;

			string[] lines = StringUtility.WordBreak(text, lineLength);

			WriteLines(lines);
		}

		public static void WriteLines(string[] lines)
		{
			foreach (string line in lines)
				Console.WriteLine(line);
		}
	}

	class Scaffold
	{
		#region Private data
		private ProgramOptions options;
		
		#endregion
	
		public Scaffold(ProgramOptions options)
		{
			this.options = options;
		}

		public int ConstructAndUseScaffold()
		{
			// Check that VS is installed and get information about it
			VisualStudioInfo vsInfo = Program.GetVisualStudioInfo();
			
			if (vsInfo == null)
				return 1;
			
			// Create a temporary directory in which to put our scaffold project
			ParsedPath projectDir;
			Random rand = new Random();

			do
			{
				projectDir = new ParsedPath(String.Format("{0}Scaffold_{1:X}", options.ScriptPath.RootAndDirectory, rand.Next()), PathType.Directory);
			}
			while (Directory.Exists(projectDir));

			Directory.CreateDirectory(projectDir);

			if (!CreateProjectFiles(vsInfo, projectDir))
				return 1;

			StartDevenvAndWait(vsInfo, new ParsedPath(
				projectDir.RootAndDirectory + options.ScriptPath.File + ".sln", PathType.File));

			DeleteProjectFiles(projectDir);
			
			return 0;
		}

		private string GetResource(string s)
		{
			Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(s);
			StreamReader sr = new StreamReader(stream, Encoding.ASCII);

			return sr.ReadToEnd();
		}

		private void CreateFile(string fileName, string content)
		{
			if (File.Exists(fileName))
			{
				Program.WriteMessage(MessageType.Warning, ScaffoldResources.AlreadyExists, fileName);
				return;
			}

			using (StreamWriter sw = new StreamWriter(fileName, false, System.Text.Encoding.ASCII))
			{
				sw.Write(content);
			}

			if (options.Verbose)
				Program.WriteMessage(MessageType.Verbose, ScaffoldResources.Created, fileName);
		}

		private void DeleteFile(string fileName)
		{
			try
			{
				if (File.Exists(fileName))
					File.Delete(fileName);
				else
					return;
			}
			catch (Exception e)
			{
				if (e is UnauthorizedAccessException || e is IOException)
				{
					Program.WriteMessage(MessageType.Warning, ScaffoldResources.CouldNotBeDeleted, fileName);
					return;
				}
				
				throw e;
			}

			if (options.Verbose)
				Program.WriteMessage(MessageType.Verbose, ScaffoldResources.Deleted, fileName);
		}

		private void DeleteDirectory(string dirName)
		{
			try
			{
				Directory.Delete(dirName, true);

				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.SubDirDeleted, dirName);
			}
			catch (SystemException)
			{
				Program.WriteMessage(MessageType.Warning, ScaffoldResources.SubDirNotDeleted, dirName);
			}
		}

		private ArrayList GrabCsProjFileReferences(string csprojFile)
		{
			ArrayList paths = new ArrayList();
			ParsedPath parsedCsprojFile = new ParsedPath(csprojFile);

			try
			{
				XPathDocument doc = new XPathDocument(csprojFile);
				XPathNavigator nav = doc.CreateNavigator();
				XmlNamespaceManager nm = new XmlNamespaceManager(nav.NameTable);

				nm.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003");

				// For all the assemblies that DO have HintPath's we should use that, because those are most
				// likely the ones that we put there, so we need to remove them.
				XPathNodeIterator iter = nav.Select("//ns:Reference/ns:HintPath/text()", nm);

				while (iter.MoveNext())
				{
					paths.Add(new ParsedPath(iter.Current.Value).MakeFullPath(parsedCsprojFile));
				}

				// TODO: To get the list of other assemblies, we can use a query like: 
				//
				//   XPathNodeIterator iter = nav.Select("//ns:Reference[not(ns:HintPath)]/@Include", nm);
				//
				// This is the Include attribute of all Reference elements with no HintPath child element.
				// This will give us a list of assembly names, but no path information.  
				// It seems like if the assembly is in the GAC it doesn't get any HintPath information.
				// We need to figure out how VS goes about finding these assemblies.
			}
			catch (XPathException)
			{
				// This would only happen if the user manually edited the .csproj file - which is a possibility
				Program.WriteMessage(MessageType.Error, ScaffoldResources.UnableToScrapeRefs, csprojFile);
			}

			return paths;
		}

		private ArrayList GrabRspFileReferences(ParsedPath rspFile)
		{
			string text;
			StringBuilder sb = new StringBuilder();

			using (StreamReader reader = new StreamReader(rspFile))
			{
				text = reader.ReadToEnd();
			}

			Regex r = new Regex("/r[^:]*:(.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);

			Match m = r.Match(text);

			ArrayList paths = new ArrayList();

			while (m.Success)
			{
				string s = m.Groups[1].Value;

				// Chop off trailing '\r'; won't be there if last line of file doesn't end in \r\n
				if (s[s.Length - 1] == '\r')
					s = s.Substring(0, s.Length - 1);

				s = Environment.ExpandEnvironmentVariables(s);

				paths.Add(new ParsedPath(s).MakeFullPath(rspFile));

				m = m.NextMatch();
			}

			return paths;
		}

		private string CreateXmlSnippetFromReferences(ArrayList paths)
		{
			StringBuilder sb = new StringBuilder();
			
			foreach (ParsedPath path in paths)
			{
				sb.AppendFormat(
					"  <Reference Include=\"{0}\">" + System.Environment.NewLine +
					"    <HintPath>{1}</HintPath>" + System.Environment.NewLine +
					"  </Reference>" + System.Environment.NewLine,
					path.File,
					path.All);
			}

			return sb.ToString();
		}

		private bool CreateProjectFiles(VisualStudioInfo vsInfo, ParsedPath projectDir)
		{
			if (!Directory.Exists(options.ScriptPath.RootAndDirectory))
			{
				Program.WriteMessage(MessageType.Error, ScaffoldResources.DirectoryDoesNotExist, options.ScriptPath.RootAndDirectory);
				return false;
			}

			// Set publicly visible script path (in this AppDomain).  This also sets the script.rsp file name.
			ScriptEnvironment.ScriptPath = options.ScriptPath;
			TaggedString ts = new TaggedString();
			ListDictionary dict = new ListDictionary();
			ParsedPath localRspFile = ScriptEnvironment.ScriptRspPath;
			ParsedPath sharedRspFile = ScriptEnvironment.SharedRspPath;
			ParsedPath runtimeRspFile = ScriptEnvironment.RuntimeRspPath;

			dict.Add("BaseName", options.ScriptPath.File);
			dict.Add("SourceDirectory", options.ScriptPath.RootAndDirectory);
			dict.Add("SourceName", options.ScriptPath.FileAndExtension);
			dict.Add("NewProjectGuid", Guid.NewGuid().ToString().ToUpper());
			
			string versionNumber = String.Format("{0}.{1}.{2}", 
				vsInfo.Version.ProductMajorPart, vsInfo.Version.ProductMinorPart, vsInfo.Version.ProductBuildPart);
			
			dict.Add("VSVersion", versionNumber);
			
			if (vsInfo.Version.ProductMajorPart == 9)
			{
			    dict.Add("ToolsVersion", " ToolsVersion=\"3.5\" ");
                dict.Add("SolutionFileVersion", "10.00");
                dict.Add("VSName", "Codename Orcas");
            }
            else
            {
                dict.Add("ToolsVersion", "");
                dict.Add("SolutionFileVersion", "9.00");
                dict.Add("VSName", "2005");
            }
            
			string refs;

			if (File.Exists(localRspFile))
			{
				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.ResponseFileProcessed, localRspFile);

				refs = CreateXmlSnippetFromReferences(GrabRspFileReferences(localRspFile));
			}
			else
				refs = "";

			dict.Add("LocalReferences", refs);

			if (File.Exists(sharedRspFile))
			{
				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.ResponseFileProcessed, sharedRspFile);

				refs = CreateXmlSnippetFromReferences(GrabRspFileReferences(sharedRspFile));
			}
			else
				refs = "";

			dict.Add("SharedReferences", refs);

			if (File.Exists(runtimeRspFile))
			{
				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.ResponseFileProcessed, runtimeRspFile);

				refs = CreateXmlSnippetFromReferences(GrabRspFileReferences(runtimeRspFile));
			}
			else
				refs = "";

			dict.Add("RuntimeReferences", refs);

			string tagProgramFile = options.ProgramTemplate;

			if (tagProgramFile == null || !File.Exists(tagProgramFile))
				tagProgramFile = GetResource("Microsoft.Tools.CodeRunner.Scaffold.Templates.Template.cs");

			string sourceFile = ts.Replace(tagProgramFile, dict);

			string tagCsProjFile = GetResource("Microsoft.Tools.CodeRunner.Scaffold.Templates.Template.csproj");
			string csProjFile = ts.Replace(tagCsProjFile, dict);

			string tagCsProjUserFile = GetResource("Microsoft.Tools.CodeRunner.Scaffold.Templates.Template.csproj.user");
			string csProjUserFile = ts.Replace(tagCsProjUserFile, dict);

			string tagSlnFile = GetResource("Microsoft.Tools.CodeRunner.Scaffold.Templates.Template.sln");
			string slnFile = ts.Replace(tagSlnFile, dict);

			try
			{
				if (!File.Exists(options.ScriptPath))
					CreateFile(options.ScriptPath, sourceFile);

				CreateFile(projectDir.RootAndDirectory + options.ScriptPath.File + ".csproj", csProjFile);
				CreateFile(projectDir.RootAndDirectory + options.ScriptPath.File + ".csproj.user", csProjUserFile);
				CreateFile(projectDir.RootAndDirectory + options.ScriptPath.File + ".sln", slnFile);
			}
			catch (IOException exp)
			{
				Program.WriteMessage(MessageType.Error, "{0}", exp.Message);
				return false;
			}

			return true;
		}

		private void RemoveDuplicateRefs(ArrayList originalRefs, ref ArrayList newRefs)
		{
			// Remove each entry in the new reference list that is in the original one
			foreach (ParsedPath path in originalRefs)
			{
				int j = newRefs.BinarySearch(path, null);

				if (j >= 0)
					newRefs.RemoveAt(j);
			}
		}

		private void DeleteProjectFiles(ParsedPath projectDir)
		{
			ParsedPath localRspFile = ScriptEnvironment.ScriptRspPath;
			ParsedPath sharedRspFile = ScriptEnvironment.SharedRspPath;
			ParsedPath runtimeRspFile = ScriptEnvironment.RuntimeRspPath;
			string projName = projectDir.RootAndDirectory + options.ScriptPath.File + ".csproj";

			if (File.Exists(projName) && options.ScrapeReferences)
			{
				ArrayList newRefs = GrabCsProjFileReferences(projName);

				// This will do an alphabetical sort of the list
				newRefs.Sort();

				// Remove all references that are already in the run-times csc.rsp
				if (File.Exists(runtimeRspFile))
				{
					RemoveDuplicateRefs(GrabRspFileReferences(runtimeRspFile), ref newRefs);
				}

				// Remove all references that are already in the shared csc.rsp
				if (File.Exists(sharedRspFile))
				{
					RemoveDuplicateRefs(GrabRspFileReferences(sharedRspFile), ref newRefs);
				}

				// Remove all references that are already in the program's .rsp
				if (File.Exists(localRspFile))
				{
					RemoveDuplicateRefs(GrabRspFileReferences(localRspFile), ref newRefs);
				}

				// If anything is left it gets added into the programs response file
				if (newRefs.Count > 0)
				{
					try
					{
						string s = String.Empty;

						if (File.Exists(localRspFile))
						{
							using (StreamReader reader = new StreamReader(localRspFile))
							{
								// Check that the file has a \r\n at the end!
								s = reader.ReadToEnd();

								if (!s.EndsWith("\r\n"))
									s += "\r\n";
							}
						}

						using (StreamWriter writer = new StreamWriter(localRspFile))
						{
							writer.Write(s);

							foreach (ParsedPath path in newRefs)
							{
								writer.WriteLine("/r:\"{0}\"", path);

								if (options.Verbose)
									Program.WriteMessage(MessageType.Verbose, ScaffoldResources.AddingToScriptRsp, path.ToString());
							}
						}
					}
					catch (IOException)
					{
						Program.WriteMessage(MessageType.Warning, ScaffoldResources.FileCouldNotBeUpdated, localRspFile);
					}
				}
			}

			DeleteDirectory(projectDir);
		}

		private bool StartDevenvAndWait(VisualStudioInfo vsInfo, ParsedPath solutionFile)
		{
			// Now try and start devenv
			Process p1 = null;

			Environment.SetEnvironmentVariable("COMPLUS_InstallRoot", "");
			Environment.SetEnvironmentVariable("COMPLUS_Version", "");

			try
			{
				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.StartingVS);

				ProcessStartInfo startInfo = new ProcessStartInfo(
					vsInfo.DevEnvExe, "\"" + solutionFile + "\" \"" + options.ScriptPath + "\"");

				startInfo.WorkingDirectory = solutionFile.RootAndDirectory;
				startInfo.UseShellExecute = false;

				p1 = Process.Start(startInfo);
			}
			catch (Win32Exception e)
			{
				Program.WriteMessage(MessageType.Error, ScaffoldResources.UnableToStartVS, solutionFile, e.Message);
				return false;
			}

			// Devenv has started.  Wait for it to exit.
			if (p1 != null)
			{
				if (options.Verbose)
					Program.WriteMessage(MessageType.Verbose, ScaffoldResources.WaitingForVS);

				p1.WaitForExit();
				p1.Close();
				p1 = null;
			}
			else
			{
				Program.WriteMessage(MessageType.Error, ScaffoldResources.VSDidNotStart);
				return false;
			}

			return true;
		}
	}
}
